""" sbak/base.py - sbak.base - Base object types.

    This module defines very basic objects used throughout the rest of
    the library. They implement metaclassing features that should make
    the development and use of ScrollBack more expedient and less
    cumbersome.
"""

import operator

class AttrView(object):
    """ Allows access to members of an object through mapping syntax.
    
        ~~~ Descriptions ~~~
        
        This class provides an alternate means of dynamic attribute
        access. The usual way is to use the the getattr, setattr,
        delattr, and hasattr builtins.
        
        This class is useful for situations where the aforementioned
        builtins must be used extensively as it reduces the complexity
        of code considerably.
    
        ~~~ How To Use ~~~
    
        Given any object `obj`, it can be written
        
        >>> obj_attrs = AttrView(obj)
        
        after which point any attribute (or property, or method, etc.)
        of `obj` may be accessed through index syntax using
        `obj_attrs`, as shown in this example:
        
        >>> obj.x = 7
        >>> obj_attrs["x"]
        7
        >>> obj_attrs["x"] = 8
        >>> obj.x
        8
    
        This will also work for properties, methods, and anything else
        that can be accessed from `obj` through normal dot syntax. So
        if a class is defined like this:
        
        >>> class Foo(object):
        ...     def get_bar(self):
        ...         return self._internal_bar
        ...     def set_bar(self, bar):
        ...         self._internal_bar = bar
        ...     bar = property(get_bar, set_bar)
        ...
        >>> obj = Foo()
        >>> obj_attrs = AttrView(obj)
        
        then the property `obj.bar` can be accessed as
        `obj_attrs["bar"]`, and the apropriate methods for the property
        will be called just as if they had been accessed for through dot
        syntax.
        
        ~~~ CREDITS ~~~
        Credit goes to Ben North and those who contributed to the
        discussion of PEP 363 for the idea and core code for this
        class. Thank you!
    """
    def __init__(self, obj):
        self.obj = obj
    
    def __getitem__(self, name):
        return getattr(self.obj, name)
      
    def __setitem__(self, name, value):
        return setattr(self.obj, name, value)
      
    def __delitem__(self, name):
        return delattr(self, name)
      
    def __contains__(self, name):
        return hasattr(self, name)

    def get(self, name, default=None):
        """ Attempt to get the attribute of the given name.
            If no such attribute exists, return the default value.
        """
        try:
            val = getattr(self.obj, name)
        except AttributeError:
            val = default
        return val

class AttrsM(type):
    
    """ Metaclass that adds the classattrs property to classes. """
    
    classattrs = property(AttrView, doc=
        """ An AttrView over the class.
        
            This returns an AttrView object that wraps the class itself
            rather than any particular instance of the class.
        """)

class AutoPropertyM(AttrsM):
    """ Metaclass for auto-generated properties from special methods.
    
        Using this as a metaclass will allow for properties to be
        created automatically based on methods that have certain
        name prefixes. It also allows for these implicitly-defined
        properties to inherit attributes from the property of the same
        name within inherited classes. This includes getters, setters,
        deleters, and docstrings.
        
        Usage:
        
        >>> class MyClass(object):
        ...     __metaclass__ = AutoPropertyM
        ... 
        ...     def _fget_region(self):
        ...         return self._region
        ...
        ...     def _fset_region(self, value):
        ...         # All new regions must be converted to Rect.
        ...         self._region = Rect(value)
        ...
        >>> class MegaClass(MyClass):
        ...
        ...     def _fset_region(self, region):
        ...         # Prevent any regions with height less than 200
        ...         if region[3] < 200:
        ...             raise ValueError("region height must be at "
        ...                              "least 200")
        ...         super(MegaClass, self)._fset_region(region)
        ...         
        
        In the example above, the first class would get a property
        called `region` with the getter defined in `_fget_region`
        and the setter defined in `_fset_region`.
        
        The second class, however, would also get a `region` property,
        and it would have the same getter as the `region` property in
        the first class, but would have the setter `_fset_region`
        defined in the second class.
        
        Effectively, then, properties are created at class definition
        with names based on the methods with names that begin with the
        _fget_ and _fset_ prefixes. The prefix _fdel_ is also
        supported.
        
        The special prefix _doc_ can be used to associate a
        docstring:
        
        ...     _doc_region = " The occupied region. "
        
        After this, the `region` property of the containing class will
        have the given string for its documentation.
        
        Note that inherited properties will get their attributes from
        the superclass's properties and NOT from the methods and class
        attributes that define those properties, assuming apropriate
        values cannot be found within the current class. In other words,
        an inherited property `foo` will get its `fget`, `fset`, etc.
        from `SuperClass.foo.fget`, `SuperClass.foo.fset`, etc. but not
        from `SuperClass._fget_foo`, `SuperClass._fset_foo`, etc. This
        allows attributes of properties to be inherited even when the
        special prefixes weren't used to create the inherited
        properties.
        
        The class's mro (method resolution order) is searched for each
        required property attribute rather than just the immediate
        bases of the current class. This means that multiple
        inheritence should (theoretically) work with automatic
        properties.
        
        The specially-prefixed methods themselves will not be altered
        in any way by this metaclass, so they can be used, for example,
        to call on the behavior of an auto-generated property's
        "parent," assuming that parent was also auto-generated.
        
        TODO: Get the docstring from the first _f*_ function that provides a docstring rather than from the _doc_ attribute.
    """
    
    def __init__(cls, name, bases, dict):
        super(AutoPropertyM, cls).__init__(name, bases, dict)
        
        # Used to break out of deep nesting later on.
        class DeepNestingBreak(Exception): pass
        
        # Move the `dict` argument to a different name to allow for access to
        # the `dict` builtin type.
        dict_ = dict
        from __builtin__ import dict
    
        Xer_groups = [
            {   'Xers' : {},
                'prefix' : '_fget_',
                'propmember' : 'fget',
                'proparg' : 'fget'
            },
                
            {   'Xers' : {},
                'prefix' : '_fset_',
                'propmember' : 'fset',
                'proparg' : 'fset'
            },
                
            {   'Xers' : {},
                'prefix' : '_del_',
                'propmember' : 'fdel',
                'proparg' : 'fdel'
            },
    
            {   'Xers' : {},
                'prefix' : '_doc_',
                'propmember' : '__doc__',
                'proparg' : 'doc'
            }
        ]
        
        # Used to find each getter, setter, etc. for each generated property.
        def find_Xer(propname, Xers, propmember, **ignored):
            
            # Default value
            Xer = None
            
            # Try to find a Xer within the current class's methods,
            # including inherited methods.
            try:
                Xer = Xers[propname]
            
            # If the current class has no Xer,
            except KeyError:
                # Try to find a Xer within the mro.
                for base in cls.__mro__:
                    try:
                    
                        # Try to find a property to get an Xer from.
                        try:
                            prop = getattr(base,propname)
                        
                        except AttributeError:
                            pass
                        
                        # If a property was actually found,
                        else:
                            potential_Xer = getattr(prop, propmember)
                            
                            # If that property has the needed Xer, use it.
                            if potential_Xer is not None:
                                Xer = potential_Xer
                                
                                raise DeepNestingBreak
                    
                    except DeepNestingBreak:
                        break;
            
            return Xer
        
        # Search class attribute names for possible Xers,
        # and put the corresponding values into each group as appopriate.
        for attrname in dir(cls):
            attr = getattr(cls, attrname)
            
            # Find attributes for each Xer group based on attr. name prefixes.
            for group in Xer_groups:
                Xers = group['Xers']
                prefix = group['prefix']
                
                # If this attribute begins with the prefix for this group,
                # put the attribute into the group.
                if attrname.startswith(prefix):
                    propname = attrname.replace(prefix, "")
                    Xers[propname] = attr
        
        # Get a set of all property names.
        propnames = set()
        for group in Xer_groups:
            propnames.update(group['Xers'].keys())
        
        # Find the Xers for each property.
        for propname in propnames:

            # Determine the arguments to pass to the `property` builtin
            args = dict(
                (g['proparg'], find_Xer(propname, **g)) for g in Xer_groups
            )

            # Produce the property.
            prop = property( **args )
            
            # Assign the property to the class
            setattr(cls,propname,prop)
    
    def generate_property(cls, name, fget=None, fset=None, fdel=None, doc=None):
        """ Generates a new property and adds it to the class.
            
            name:   Name of the new property.
        
            fget:   A function for getting the value.
            
            fset:   A function for setting the value.
            
            fdel:   A function for deleting the value.
            
            doc:    A docstring to describe the property.
        """
        cls.classattrs[name] = property(fget, fset, fdel, doc)

class LinkableM(AutoPropertyM):
    """ Metaclass that enforces "object linking."
        
        Object linking is when two objects have a bi-directional link
        between each other that both objects are aware of and which can
        be severed at any time by either object.
        
        Each linked object can be accessed via a property of the other.
        This property will have a name that describes what kind of
        relationship the object has to the other--effectively, the
        reason that the objects are linked.
        
        The class of each linkable object must know what kinds of
        relationships it supports before 
        
        For example, consider the relationship between a cat and a
        mouse. The former is the predator, the latter is the prey.
        Therefore, the mouse can be referred to as `cat.prey`, and
        the cat can be referred to as `mouse.predator`:
        
        >>> cat.prey == mouse
        True
        >>> mouse.predator == cat
        True
        
        Because the relationship between the two objects is known to
        each, both will know as soon as the link is broken, regardless
        of which object broke the link. This can be seen in the
        following code:
        
        >>> cat.prey = None
        >>> mouse.predator
        None
        
        Even though the mouse's `predator` member was not touched
        directly, it knew that it was no longer the cat's prey because
        the cat's link to it was broken.
        
        Alternately, suppose the cat was chasing the mouse, but then
        spotted a bird. This could be acheived symbolically through the
        following code, which continues from the previous example:
        
        >>> mouse.predator      # Mouse is currently safe
        None
        >>> cat.prey = mouse    # Cat sees the mouse
        >>> mouse.predator      # Mouse is now being chased by the cat
        <Cat object at 0x...>
        >>> cat.prey = bird     # Cat sees the bird and forgets the mouse
        >>> bird.predator       # Now the bird is the one in danger,
        <Cat object at 0x...>
        >>> mouse.predator      # so the mouse is safe again.
        None
        
        This behavior is useful for enforcing complex tree- or net-like
        structures of objects by helping to to make sure that no
        "dangling references" occur due to incorrectly updating
        references among several objects.
        
        Any two objects using the LinkableM metaclass can be linked
        together, but their classes must indicate how the objects link.
        This is as simple as defining a class attribute like this:
        
        >>> class Cat(object):
        ...     # Allow Cat objects to be linkable
        ...     __metaclass__ = LinkableM
        ...    
        ...     # Now indicate what kind of links Cats can have.
        ...     __links = {
        ...         "prey" : "predator"
        ...     }
        ...
        >>> class Mouse(object):
        ...     # Allow mice (Mouses) to be linkable.
        ...     __metaclass__ = LinkableM
        ...    
        ...     # Now indicate what kind of links mice can have.
        ...     __links = {
        ...         "predator" : "prey"
        ...     }
        
        As shown above, the `links` attribute on linkable classes is a
        dictionary that maps the names of the relationships of the
        "other" object to the names of the relationships of the "self"
        object. For objects to be linkable, the mapping of
        relationship names for one object's type must be the inverse of
        the other object's type's.
        
        It should be obvious by now, but one object can be linked to
        more than one other object at a time, though there must be a
        unique "reason why" each other object is linked to the first.
        For example, if we add a Human class, a Cat could be its pet,
        while at the same time still chasing the mouse. We could even
        go one step further and describe the relationship between the
        mouse and the human:
        
        >>> class Cat(object):
        ...     ...
        ...     __links = {
        ...         "prey" : "predator",
        ...         "owner" : "pet"
        ...     }
        ...
        >>> class Mouse(object):
        ...     ...
        ...     __links = {
        ...         "predator" : "predator",
        ...         "host" : "pest"
        ...     }
        ...
        >>> class Human(object):
        ...     ...
        ...     __links = {
        ...         "pet" : "owner",
        ...         "pest" : "host"
        ...     }
        ...
        >>> cat = Cat()
        >>> mouse = Mouse()
        >>> human = Human()
        >>>
        >>> cat.owner = human
        >>> cat.prey = mouse
        >>> mouse.host = human
        >>>
        >>> mouse.predator == cat
        True
        >>> human.pet == cat
        True
        >>> human.pest == mouse
        True
        >>>
        >>> human.pest = None
        >>> human.pet = None
        >>> mouse.predator = None
        >>>
        >>> cat.prey == mouse
        False
        >>> cat.owner == human
        False
        >>> mouse.host == human
        False
        
        The effects exemplified above are acheived through
        automatically-generated methods and properties that the
        LinkableM metaclass creates for its classes. When a class is
        built, the metaclass checks for an attribute called "__links",
        which must be a dictionary or other object that implements the
        `.iteritems()` method. Each value in the dict is made into a
        property that, when set, will try to call a method named
        "set_<VALUE>_internal" on the new linked object. Also, a
        "set_<KEY>_internal" method will be created.
    """
    def __init__(cls, name, bases, dict):
        super(LinkableM,cls).__init__(name, bases, dict)
        
        cls_attrs = cls.classattrs
        
        # Find links
        
        try:
            links = cls_attrs['_'+name+'__links']
        except AttributeError:
            pass
        else:
            for other_name, self_name in links.iteritems():
                # Name of self's internal variable containing a reference
                # to the other
                _other_name = '_'+other_name
                
                # Getter that returns a setter that sets the other's internal
                # reference to self
                get_other_set_other_internal = operator.attrgetter(
                    '_set_'+self_name+'_internal')
                
                # Getter that returns an unsetter that unsets the other's
                # internal reference to self
                get_other_unset_other_internal = operator.attrgetter(
                    '_unset_'+self_name+'_internal')
                
                # Getter that returns self's other
                _fget_other = operator.attrgetter(_other_name)
                
                # Setter that sets self's internal reference to the other
                def _set_other_internal(self,val):
                    other = getattr(self, _other_name)
                    if other is not val and other is not None:
                        get_other_unset_other_internal(other)()
                    setattr(self, _other_name, val)
                _set_other_internal.__name__ =\
                    '_set_'+other_name+'_internal'
                
                # Unsetter that unsets self's internal reference to the other
                def _unset_other_internal(self):
                    setattr(self,_other_name,None)
                _unset_other_internal.__name__ =\
                    '_unset_'+other_name+'_internal'
                
                # Setter that sets self's reference to the other and also
                # sets the other's reference to self.
                def _fset_other(self,val):
                    if val is not None:
                        try:
                            other_set_other_internal =\
                                get_other_set_other_internal(val)
                        except AttributeError:
                            raise TypeError("%r is not a valid %s for %r." %
                                (val, other_name, self))
                        else:
                            other_set_other_internal(self)
                    _set_other_internal(self,val)
                _fset_other.__name__ = '_fset_'+other_name
                
                _doc_other =\
                """ The %(other)s for this %(self)s.
                
                    Setting the value of this property to an object will
                    attempt to use that object's `set_%(self)s_internal` method
                    to change the object's %(self)s to self. If the object
                    does not have a method called "set_%(self)s_internal`, the
                    object is considered an "invalid %(other)s" and a TypeError
                    will be raised. (This does not occur when the object is
                    None, however.)
                """ % {"self" : self_name, "other" : other_name}
                
                # Property that combines self's getter and setter
                p = property(_fget_other, _fset_other, None, _doc_other)
                
                # Add the members to the class
                cls_attrs[_other_name] = None
                cls_attrs[other_name] = p
                cls_attrs[_set_other_internal.func_name] =\
                    _set_other_internal.__get__(None,cls)
                cls_attrs[_unset_other_internal.func_name] =\
                    _unset_other_internal.__get__(None,cls)

class ObjectM(LinkableM):
    """ Metaclass for ScrollBack's base Object class.
    
        This should remain the metaclass for Object even if it does not
        extend its base metaclasses in any way.
    """
    pass

class Object(object):
    """ The base class for all other sbak classes (except metaclasses).
    
        This class uses ObjectM as its metaclass, which means subclass
        definitions have access to the following features:
        
        * Class-level AttrView -- See AttrM
        * Linkability -- See LinkableM
        * Automatically-generated properties -- See AutoPropertyM
        
        Additionally, this class defines a single property:
        
        attrs:      Returns an AttrView object that wraps the current
                    object of the class. See AttrView for details.
    """
    __metaclass__ = ObjectM
    
    attrs = property(AttrView, doc=
        """ Returns an Attrs instance that wraps this object.
            See `sbak.base.Attrs` for more information.
        """
    )

#class ChildrenSequence(Object):
#    __links = {"parent" : "children"}
#    
#    def __init__(self, parent, values):
#        self.parent = parent
#        self._values = list(values)
#    
#    def __getitem__(self, index):
#        return self._values[index]
#        
#    def __setitem__(self, index):
#        
#        
#    def __delitem__(self, index):
#    
#    def __iter__(self)
#    
#    def __contains__(self, value):
#        
#    def __len__(self):
#        
#    def append_internal(self, index, value):
#    
#    def append(self, value):
#        
#    def add(self, value):
#        
#    def insert(self, index, value):
#        
#    def remove_internal(self, value):
#        
#    def remove(self, value):
#        
#    def discard(self, value):
#    
#
#class SubsSequence(Object):
#    
#    __links = {"parent" : "subs"}
#    
#    def __init__(self, parent, values):
#        self.parent = parent
#        self._values = list(values)
#    
#    def __getitem__(self, index):
#        return self._values[index]
#        
#    def __setitem__(self, index):
#        
#        
#    def __delitem__(self, index):
#    
#    def __iter__(self)
#    
#    def __contains__(self, value):
#        
#    def __len__(self):
#        
#    def append_internal(self, index, value):
#    
#    def append(self, value):
#        
#    def add(self, value):
#        
#    def insert(self, index, value):
#        
#    def remove_internal(self, value):
#        
#    def remove(self, value):
#        
#    def discard(self, value):
#
#class Parent(Object):
#    __links = {
#        "children" : "parent",
#        "subs" : "parent"
#    }
#    
#    def __init__(self):
#        self._children = ChildrenSequence(self, [])
#        self._subs = SubsSequence()
#    
#    def add_child(self, child):
#        try:
#            set_parent_internal = child.set_parent_internal
#        except AttributeError:
#            raise TypeError("object %r does not support changing parent." %
#                            child)
#        else:
#            set_parent_internal(self)
#            self.add_child_internal(child)
#        
#    def add_child_internal(self, child):
#        if child not in self._children:
#            self._children.append(child)
#        
#    def discard_child(self, child):
#        try:
#            unset_parent_internal = child.unset_parent_internal()
#        except AttributeError:
#            raise TypeError("object %r does not support unsetting parent. "
#                            "(Is the object really a child of this parent?)"
#                            % child)
#        else:
#            unset_parent_internal()
#            self.discard_child_internal(child)
#        
#    def discard_child_internal(self, child):
#        if child in self._children:
#            self._children.remove(child)
#
#class Child(Object):
#    def __init__(self):
#        self._parent = None
#        
#    def set_parent(self, parent):
#        try:
#            add_child_internal = parent.add_child_internal
#        except AttributeError:
#            raise TypeError("object %r does not support adding children."
#                            % parent)
#        else:
#            self.set_parent_internal(parent)
#            add_child_internal(self)
#        
#    def set_parent_internal(self, parent):
#        old = self._parent
#        if old is not None:
#            old.discard_child_internal(self)
#        self._parent = parent
#        
#    def unset_parent_internal(self):
#        self._parent = None
#
#class Sub(Object):
#    def __init__(self):
#        self._parent = None
#        
#    def set_parent(self, parent):
#        try:
#            add_sub_internal = parent.add_sub_internal
#        except AttributeError:
#            raise TypeError("object %r does not support adding subs."
#                            % parent)
#        else:
#            self.set_parent_internal(parent)
#            add_sub_internal(self)
#        
#    def set_parent_internal(self, parent):
#        old = self._parent
#        if old is not None:
#            old.discard_sub_internal(self)
#        self._parent = parent
#        
#    def unset_parent_internal(self):
#        self._parent = None